<?php

if (!defined('BASEPATH'))
    exit('No direct script access allowed');



/**
 * CodeIgniter
 *
 * An open source application development framework for PHP 5.1.6 or newer
 *
 * @package		CodeIgniter
 * @author		ExpressionEngine Dev Team
 * @copyright	Copyright (c) 2008 - 2011, EllisLab, Inc.
 * @license		http://codeigniter.com/user_guide/license.html
 * @link		http://codeigniter.com
 * @since		Version 1.0
 * @filesource
 */
// ------------------------------------------------------------------------

/**
 * SQLSRV Database Adapter Class
 *
 * Note: _DB is an extender class that the app controller
 * creates dynamically based on whether the active record
 * class is being used or not.
 *
 * @package		CodeIgniter
 * @subpackage	Drivers
 * @category	Database
 * @author		ExpressionEngine Dev Team
 * @link		http://codeigniter.com/user_guide/database/
 */
class CI_DB_sqlsrv_driver extends CI_DB {

    var $dbdriver = 'sqlsrv';
    // The character used for escaping
    var $_escape_char = '';
    // clause and character used for LIKE escape sequences
    var $_like_escape_str = " ESCAPE '%s' ";
    var $_like_escape_chr = '!';

    /**
     * The syntax to count rows is slightly different across different
     * database engines, so this string appears in each driver and is
     * used for the count_all() and count_all_results() functions.
     */
    var $_count_string = "SELECT COUNT(*) AS ";
    var $_random_keyword = ' ASC'; // not currently supported

    /*
     * SQL serv prepared query backend.
     */
    var $option = array('Scrollable' => SQLSRV_CURSOR_STATIC, 'SendStreamParamsAtExec' => true);
    var $sqlsrv_sql = "";
    var $sqlsrv_param = array();
    var $sqlsrv_where = array();
    var $sqlsrv_where_param = array();
    var $sqlsrv_like = array();
    var $sqlsrv_like_param = array();
    var $sqlsrv_having = array();
    var $sqlsrv_having_param = array();

    /**
     * Non-persistent database connection
     *
     * @access	private called by the base class
     * @return	resource
     */
    function db_connect($pooling = false) {
        // Check for a UTF-8 charset being passed as CI's default 'utf8'.
        $character_set = (0 === strcasecmp('utf8', $this->char_set)) ? 'UTF-8' : $this->char_set;

        $connection = array(
            'UID' => empty($this->username) ? '' : $this->username,
            'PWD' => empty($this->password) ? '' : $this->password,
            'Database' => $this->database,
            'ConnectionPooling' => $pooling ? 1 : 0,
            'CharacterSet' => $character_set,
            'ReturnDatesAsStrings' => 1
        );

        // If the username and password are both empty, assume this is a
        // 'Windows Authentication Mode' connection.
        if (empty($connection['UID']) && empty($connection['PWD'])) {
            unset($connection['UID'], $connection['PWD']);
        }

        return sqlsrv_connect($this->hostname, $connection);
    }

    // --------------------------------------------------------------------

    /**
     * Persistent database connection
     *
     * @access	private called by the base class
     * @return	resource
     */
    function db_pconnect() {
        return $this->db_connect(TRUE);
    }

    // --------------------------------------------------------------------

    /**
     * Reconnect
     *
     * Keep / reestablish the db connection if no queries have been
     * sent for a length of time exceeding the server's idle timeout
     *
     * @access	public
     * @return	void
     */
    function reconnect() {
        // not implemented in MSSQL
    }

    // --------------------------------------------------------------------

    /**
     * Select the database
     *
     * @access	private called by the base class
     * @return	resource
     */
    function db_select() {
        $sql = "USE " . $this->database;
        $stmt = sqlsrv_prepare($this->conn_id, $sql, null, $this->option);
        return sqlsrv_execute($stmt);
    }

    // --------------------------------------------------------------------

    /**
     * Set client character set
     *
     * @access	public
     * @param	string
     * @param	string
     * @return	resource
     */
    function db_set_charset($charset, $collation) {
        // @todo - add support if needed
        return TRUE;
    }

    // --------------------------------------------------------------------

    /**
     * Execute the query
     *
     * @access	private called by the base class
     * @param	string	an SQL query
     * @return	resource
     */
    function _execute($sql) {
        if (empty($this->sqlsrv_sql)) {
            $this->sqlsrv_sql = $sql;
        }
        if (!(is_array($this->sqlsrv_param) && count($this->sqlsrv_param) > 0)) {
            $params = null;
        } else {
            $params = array();
            foreach ($this->sqlsrv_param as &$param) {
                $params[] = $param;
            }
        }

        // Debug
        /*
          echo $this->sqlsrv_sql . "\r\n";
          foreach ($this->sqlsrv_param as $v) {
          echo $v . " ";
          }
          echo "\r\n";
         */
        if ($this->is_write_type($sql)) {
            $result = sqlsrv_prepare($this->conn_id, $this->sqlsrv_sql, $params, $this->option);
            sqlsrv_execute($result);
        } else {
            $result = sqlsrv_query($this->conn_id, $this->sqlsrv_sql, $params, $this->option);
        }

        // reset dummy
        $this->sqlsrv_sql = "";
        $this->sqlsrv_param = array();
        $this->sqlsrv_where = array();
        $this->sqlsrv_where_param = array();
        $this->sqlsrv_having = array();
        $this->sqlsrv_having_param = array();
        return $result;
    }

    // --------------------------------------------------------------------

    /**
     * Prep the query
     *
     * If needed, each database adapter can prep the query string
     *
     * @access	private called by execute()
     * @param	string	an SQL query
     * @return	string
     */
    function _prep_query($sql) {
        return $sql;
    }

    // --------------------------------------------------------------------

    /**
     * Begin Transaction
     *
     * @access	public
     * @return	bool
     */
    function trans_begin($test_mode = FALSE) {
        if (!$this->trans_enabled) {
            return TRUE;
        }

        // When transactions are nested we only begin/commit/rollback the outermost ones
        if ($this->_trans_depth > 0) {
            return TRUE;
        }

        // Reset the transaction failure flag.
        // If the $test_mode flag is set to TRUE transactions will be rolled back
        // even if the queries produce a successful result.
        $this->_trans_failure = ($test_mode === TRUE) ? TRUE : FALSE;

        return sqlsrv_begin_transaction($this->conn_id);
    }

    // --------------------------------------------------------------------

    /**
     * Commit Transaction
     *
     * @access	public
     * @return	bool
     */
    function trans_commit() {
        if (!$this->trans_enabled) {
            return TRUE;
        }

        // When transactions are nested we only begin/commit/rollback the outermost ones
        if ($this->_trans_depth > 0) {
            return TRUE;
        }

        return sqlsrv_commit($this->conn_id);
    }

    // --------------------------------------------------------------------

    /**
     * Rollback Transaction
     *
     * @access	public
     * @return	bool
     */
    function trans_rollback() {
        if (!$this->trans_enabled) {
            return TRUE;
        }

        // When transactions are nested we only begin/commit/rollback the outermost ones
        if ($this->_trans_depth > 0) {
            return TRUE;
        }

        return sqlsrv_rollback($this->conn_id);
    }

    // --------------------------------------------------------------------

    /**
     * Escape String
     *
     * @access	public
     * @param	string
     * @param	bool	whether or not the string will be used in a LIKE condition
     * @return	string
     */
    function escape_str($str, $like = FALSE) {
        // Escape single quotes
        return str_replace("'", "''", $str);
    }

    // --------------------------------------------------------------------

    /**
     * Affected Rows
     *
     * @access	public
     * @return	integer
     */
    function affected_rows() {
        return sqlsrv_rows_affected($this->result_id);
    }

    // --------------------------------------------------------------------

    /**
     * Insert ID
     *
     * Returns the last id created in the Identity column.
     *
     * @access public
     * @return integer
     */
    function insert_id() {
        return $this->query('select @@IDENTITY as insert_id')->row('insert_id');
    }

    // --------------------------------------------------------------------

    /**
     * Parse major version
     *
     * Grabs the major version number from the
     * database server version string passed in.
     *
     * @access private
     * @param string $version
     * @return int16 major version number
     */
    function _parse_major_version($version) {
        preg_match('/([0-9]+)\.([0-9]+)\.([0-9]+)/', $version, $ver_info);
        return $ver_info[1]; // return the major version b/c that's all we're interested in.
    }

    // --------------------------------------------------------------------

    /**
     * Version number query string
     *
     * @access public
     * @return string
     */
    function _version() {
        $info = sqlsrv_server_info($this->conn_id);
        return sprintf("select '%s' as ver", $info['SQLServerVersion']);
    }

    // --------------------------------------------------------------------

    /**
     * "Count All" query
     *
     * Generates a platform-specific query string that counts all records in
     * the specified database
     *
     * @access	public
     * @param	string
     * @return	string
     */
    function count_all($table = '') {
        if ($table == '')
            return '0';

        $query = $this->query("SELECT COUNT(*) AS numrows FROM " . $this->dbprefix . $table);

        if ($query->num_rows() == 0)
            return '0';

        $row = $query->row();
        return $row->numrows;
    }

    // --------------------------------------------------------------------

    /**
     * List table query
     *
     * Generates a platform-specific query string so that the table names can be fetched
     *
     * @access	private
     * @param	boolean
     * @return	string
     */
    function _list_tables($prefix_limit = FALSE) {
        return "SELECT name FROM sysobjects WHERE type = 'U' ORDER BY name";
    }

    // --------------------------------------------------------------------

    /**
     * List column query
     *
     * Generates a platform-specific query string so that the column names can be fetched
     *
     * @access	private
     * @param	string	the table name
     * @return	string
     */
    function _list_columns($table = '') {
        return "SELECT * FROM INFORMATION_SCHEMA.Columns WHERE TABLE_NAME = '" . $this->_escape_table($table) . "'";
    }

    // --------------------------------------------------------------------

    /**
     * Field data query
     *
     * Generates a platform-specific query so that the column data can be retrieved
     *
     * @access	public
     * @param	string	the table name
     * @return	object
     */
    function _field_data($table) {
        return "SELECT TOP 1 * FROM " . $this->_escape_table($table);
    }

    // --------------------------------------------------------------------

    /**
     * The error message string
     *
     * @access	private
     * @return	string
     */
    function _error_message() {
        $error = array_shift(sqlsrv_errors());
        return!empty($error['message']) ? $error['message'] : null;
    }

    // --------------------------------------------------------------------

    /**
     * The error message number
     *
     * @access	private
     * @return	integer
     */
    function _error_number() {
        $error = array_shift(sqlsrv_errors());
        return isset($error['SQLSTATE']) ? $error['SQLSTATE'] : null;
    }

    // --------------------------------------------------------------------

    /**
     * Escape Table Name
     *
     * This function adds backticks if the table name has a period
     * in it. Some DBs will get cranky unless periods are escaped
     *
     * @access	private
     * @param	string	the table name
     * @return	string
     */
    function _escape_table($table) {
        return $table;
    }

    /**
     * Escape the SQL Identifiers
     *
     * This function escapes column and table names
     *
     * @access	private
     * @param	string
     * @return	string
     */
    function _escape_identifiers($item) {
        return $item;
    }

    // --------------------------------------------------------------------

    /**
     * From Tables
     *
     * This function implicitly groups FROM tables so there is no confusion
     * about operator precedence in harmony with SQL standards
     *
     * @access	public
     * @param	type
     * @return	type
     */
    function _from_tables($tables) {
        if (!is_array($tables)) {
            $tables = array($tables);
        }

        return implode(', ', $tables);
    }

    // --------------------------------------------------------------------

    /**
     * Insert statement
     *
     * Generates a platform-specific insert string from the supplied data
     *
     * @access	public
     * @param	string	the table name
     * @param	array	the insert keys
     * @param	array	the insert values
     * @return	string
     */
    function _insert($table, $keys, $values) {
        return "INSERT INTO " . $this->_escape_table($table) . " (" . implode(', ', $keys) . ") VALUES (" . implode(', ', $values) . ")";
    }

    // --------------------------------------------------------------------

    /**
     * Insert_batch statement
     *
     * Generates a platform-specific insert string from the supplied data
     *
     * @access	public
     * @param	string	the table name
     * @param	array	the insert keys
     * @param	array	the insert values
     * @return	string
     */
    function _insert_batch($table, $keys, $values) {
        return "INSERT INTO " . $table . " (" . implode(', ', $keys) . ") VALUES " . implode(', ', $values);
    }

    // --------------------------------------------------------------------

    /**
     * Update statement
     *
     * Generates a platform-specific update string from the supplied data
     *
     * @access	public
     * @param	string	the table name
     * @param	array	the update data
     * @param	array	the where clause
     * @param	array	the orderby clause
     * @param	array	the limit clause
     * @return	string
     */
    function _update($table, $values, $where) {
        foreach ($values as $key => $val) {
            $valstr[] = $key . " = " . $val;
        }
        return "UPDATE " . $this->_escape_table($table) . " SET " . implode(', ', $valstr) . " WHERE " . implode(" ", $where);
    }

    // --------------------------------------------------------------------

    /**
     * Truncate statement
     *
     * Generates a platform-specific truncate string from the supplied data
     * If the database does not support the truncate() command
     * This function maps to "DELETE FROM table"
     *
     * @access	public
     * @param	string	the table name
     * @return	string
     */
    function _truncate($table) {
        return "TRUNCATE TABLE " . $table;
    }

    // --------------------------------------------------------------------

    /**
     * Delete statement
     *
     * Generates a platform-specific delete string from the supplied data
     *
     * @access	public
     * @param	string	the table name
     * @param	array	the where clause
     * @param	string	the limit clause
     * @return	string
     */
    function _delete($table, $where) {
        return "DELETE FROM " . $this->_escape_table($table) . " WHERE " . implode(" ", $where);
    }

    // --------------------------------------------------------------------

    /**
     * Limit string
     *
     * Generates a platform-specific LIMIT clause
     *
     * @access	public
     * @param	string	the sql query string
     * @param	integer	the number of rows to limit the query to
     * @param	integer	the offset value
     * @return	string
     */
    function _limit($sql, $limit, $offset) {
        // not implemented in MSSQL
    }

    // --------------------------------------------------------------------

    /**
     * Close DB Connection
     *
     * @access	public
     * @param	resource
     * @return	void
     */
    function _close($conn_id) {
        if (is_resource($this->result_id)) {
            sqlsrv_free_stmt($this->result_id);
            $this->result_id = FALSE;
        }

        @sqlsrv_close($conn_id);
    }

    // --------------------------------------------------------------------

    /**
     * Compile the SELECT statement
     *
     * Generates a query string based on which functions were used.
     * Should not be called directly.  The get() function calls it.
     *
     * @return	string
     */
    protected function _compile_select($select_override = FALSE) {
        // Combine any cached components with the current statements
        $this->_merge_cache();

        $has_limit = is_numeric($this->ar_limit);
        $has_order_by = count($this->ar_orderby) > 0;

        if ($has_limit === TRUE && $has_order_by !== TRUE) {
            return $this->display_error('db_invalid_query');
        }

        $order_by = "";
        if ($has_order_by === TRUE) {
            $order_by .= "\nORDER BY ";
            $order_by .= implode(', ', $this->ar_orderby);
            if ($this->ar_order !== FALSE) {
                $order_by .= ($this->ar_order == 'desc') ? ' DESC' : ' ASC';
            }
        }

        // ----------------------------------------------------------------
        // Write the "select" portion of the query

        if ($select_override !== FALSE) {
            $sql = $select_override;
            $this->sqlsrv_sql = $select_override;
        } else {
            $sql = (!$this->ar_distinct) ? 'SELECT ' : 'SELECT DISTINCT ';
            $this->sqlsrv_sql = (!$this->ar_distinct) ? 'SELECT ' : 'SELECT DISTINCT ';

            if (count($this->ar_select) == 0) {
                $sql .= '*';
                $this->sqlsrv_sql .= '*';
            } else {
                // Cycle through the "select" portion of the query and prep each column name.
                // The reason we protect identifiers here rather then in the select() function
                // is because until the user calls the from() function we don't know if there are aliases
                foreach ($this->ar_select as $key => $val) {
                    $no_escape = isset($this->ar_no_escape[$key]) ? $this->ar_no_escape[$key] : NULL;
                    $this->ar_select[$key] = $this->_protect_identifiers($val, FALSE, $no_escape);
                }

                $sql .= implode(', ', $this->ar_select);
                $this->sqlsrv_sql .= implode(', ', $this->ar_select);
            }
        }

        if ($has_limit === TRUE) {
            $sql .= ', ROW_NUMBER() OVER (' . $order_by . ') AS _row_number_ ';
            $this->sqlsrv_sql .= ', ROW_NUMBER() OVER (' . $order_by . ') AS _row_number_ ';
        }

        // ----------------------------------------------------------------
        // Write the "FROM" portion of the query

        if (count($this->ar_from) > 0) {
            $sql .= "\nFROM ";
            $sql .= $this->_from_tables($this->ar_from);

            $this->sqlsrv_sql .= "\nFROM ";
            $this->sqlsrv_sql .= $this->_from_tables($this->ar_from);
        }

        // ----------------------------------------------------------------
        // Write the "JOIN" portion of the query

        if (count($this->ar_join) > 0) {
            $sql .= "\n";
            $sql .= implode("\n", $this->ar_join);

            $this->sqlsrv_sql .= "\n";
            $this->sqlsrv_sql .= implode("\n", $this->ar_join);
        }

        // ----------------------------------------------------------------
        // Write the "WHERE" portion of the query

        if (count($this->ar_where) > 0 OR count($this->ar_like) > 0) {
            $sql .= "\nWHERE ";
            $this->sqlsrv_sql .= "\nWHERE ";
        }

        $sql .= implode("\n", $this->ar_where);
        $this->sqlsrv_sql .= implode("\n", $this->sqlsrv_where);

        // ----------------------------------------------------------------
        // Write the "LIKE" portion of the query

        if (count($this->ar_like) > 0) {
            if (count($this->ar_where) > 0) {
                $sql .= "\nAND ";
                $this->sqlsrv_sql .= "\nAND ";
            }
            $sql .= implode("\n", $this->ar_like);
            $this->sqlsrv_sql .= implode("\n", $this->sqlsrv_like);
        }

        // ----------------------------------------------------------------
        // Write the "GROUP BY" portion of the query

        if (count($this->ar_groupby) > 0) {
            $sql .= "\nGROUP BY ";
            $sql .= implode(', ', $this->ar_groupby);

            $this->sqlsrv_sql .= "\nGROUP BY ";
            $this->sqlsrv_sql .= implode(', ', $this->ar_groupby);
        }

        // ----------------------------------------------------------------
        // Write the "HAVING" portion of the query

        if (count($this->ar_having) > 0) {
            $sql .= "\nHAVING ";
            $sql .= implode("\n", $this->ar_having);

            $this->sqlsrv_sql .= "\nHAVING ";
            $this->sqlsrv_sql .= implode("\n", $this->sqlsrv_having);
        }

        // ----------------------------------------------------------------
        // Write the "ORDER BY" portion of the query
        // Write the "LIMIT" portion of the query

        if (is_numeric($this->ar_limit)) {
            $offset = empty($this->ar_offset) ? 0 : $this->ar_offset;

            $sub = $sql;
            $sql = "SELECT TOP " . $this->ar_limit . " * FROM (";
            $sql .= "\n" . $sub;
            $sql .= "\n) AS _sub_";
            $sql .= "\nWHERE _sub_._row_number_ > " . $offset;
            $sql .= "\nORDER BY _sub_._row_number_ ASC";

            $sub = $this->sqlsrv_sql;
            $this->sqlsrv_sql = "SELECT TOP " . $this->ar_limit . " * FROM (";
            $this->sqlsrv_sql .= "\n" . $sub;
            $this->sqlsrv_sql .= "\n) AS _sub_";
            $this->sqlsrv_sql .= "\nWHERE _sub_._row_number_ > " . $offset;
            $this->sqlsrv_sql .= "\nORDER BY _sub_._row_number_ ASC";
        } else {
            $sql .= $order_by;
            $this->sqlsrv_sql .= $order_by;
        }

        // parameter marge
        $this->sqlsrv_param = array_merge($this->sqlsrv_where_param, $this->sqlsrv_like_param, $this->sqlsrv_having_param);

        return $sql;
    }

    protected function _where($key, $value = NULL, $type = 'AND ', $escape = NULL) {
        if (!is_array($key)) {
            $key = array($key => $value);
        }

        // If the escape value was not set will will base it on the global setting
        if (!is_bool($escape)) {
            $escape = $this->_protect_identifiers;
        }

        foreach ($key as $k => $v) {
            $prefix = (count($this->ar_where) == 0 AND count($this->ar_cache_where) == 0) ? '' : $type;

            if (is_null($v) && !$this->_has_operator($k)) {
                // value appears not to have been set, assign the test to IS NULL
                $k .= ' IS NULL';
            }

            if (!is_null($v)) {
                if ($escape === TRUE) {
                    $k = $this->_protect_identifiers($k, FALSE, $escape);

                    $v = ' ' . $this->escape($v);
                }

                if (!$this->_has_operator($k)) {
                    $k .= ' = ';
                }
            } else {
                $k = $this->_protect_identifiers($k, FALSE, $escape);
            }

            $this->ar_where[] = $prefix . $k . $v;

            $this->sqlsrv_where[] = $prefix . $k . '?';
            $this->sqlsrv_where_param[] = $value;

            if ($this->ar_caching === TRUE) {
                $this->ar_cache_where[] = $prefix . $k . $v;
                $this->ar_cache_exists[] = 'where';
            }
        }

        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Where_in
     *
     * Generates a WHERE field IN ('item', 'item') SQL query joined with
     * AND if appropriate
     *
     * @param	string	The field to search
     * @param	array	The values searched on
     * @return	object
     */
    protected function _where_in($key = NULL, $values = NULL, $not = FALSE, $type = 'AND ') {
        if ($key === NULL OR $values === NULL) {
            return;
        }

        if (!is_array($values)) {
            $values = array($values);
        }

        $not = ($not) ? ' NOT' : '';

        foreach ($values as $value) {
            $this->ar_wherein[] = $this->escape($value);
        }

        $prefix = (count($this->ar_where) == 0) ? '' : $type;

        $where_in = $prefix . $this->_protect_identifiers($key) . $not . " IN (" . implode(", ", $this->ar_wherein) . ") ";

        $this->ar_where[] = $where_in;
        foreach ($this->ar_wherein as $param) {
            $this->sqlsrv_where_param[] = $param;
        }
        $this->sqlsrv_where[] = $prefix . $this->_protect_identifiers($key) . $not . " IN (" . implode(", ", array_fill_keys($this->ar_wherein, '?')) . ") ";
        if ($this->ar_caching === TRUE) {
            $this->ar_cache_where[] = $where_in;
            $this->ar_cache_exists[] = 'where';
        }

        // reset the array for multiple calls
        $this->ar_wherein = array();
        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Like
     *
     * Called by like() or orlike()
     *
     * @param	mixed
     * @param	mixed
     * @param	string
     * @return	object
     */
    protected function _like($field, $match = '', $type = 'AND ', $side = 'both', $not = '') {
        if (!is_array($field)) {
            $field = array($field => $match);
        }

        foreach ($field as $k => $v) {
            $k = $this->_protect_identifiers($k);

            $prefix = (count($this->ar_like) == 0) ? '' : $type;

            $dummy_param = $v;
            $v = $this->escape_like_str($v);

            if ($side == 'before') {
                $like_statement = $prefix . " $k $not LIKE '%{$v}'";
                $dummy_param = '%' . $dummy_param;
            } elseif ($side == 'after') {
                $like_statement = $prefix . " $k $not LIKE '{$v}%'";
                $dummy_param = $dummy_param . '%';
            } else {
                $like_statement = $prefix . " $k $not LIKE '%{$v}%'";
                $dummy_param = '%' . $dummy_param . '%';
            }

            $dummy_statement = $prefix . " $k $not LIKE ?";
            // some platforms require an escape sequence definition for LIKE wildcards
            if ($this->_like_escape_str != '') {
                $like_statement = $like_statement . sprintf($this->_like_escape_str, $this->_like_escape_chr);
                $dummy_statement = $dummy_statement . sprintf($this->_like_escape_str, $this->_like_escape_chr);
            }

            $this->sqlsrv_like_param[] = $dummy_param;
            $this->sqlsrv_like[] = $dummy_statement;

            $this->ar_like[] = $like_statement;
            if ($this->ar_caching === TRUE) {
                $this->ar_cache_like[] = $like_statement;
                $this->ar_cache_exists[] = 'like';
            }
        }
        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Sets the HAVING values
     *
     * Called by having() or or_having()
     *
     * @param	string
     * @param	string
     * @return	object
     */
    protected function _having($key, $value = '', $type = 'AND ', $escape = TRUE) {
        if (!is_array($key)) {
            $key = array($key => $value);
        }

        foreach ($key as $k => $v) {
            $this->sqlsrv_having_param[] = $v;
            $prefix = (count($this->ar_having) == 0) ? '' : $type;

            if ($escape === TRUE) {
                $k = $this->_protect_identifiers($k);
            }

            if (!$this->_has_operator($k)) {
                $k .= ' = ';
            }

            if ($v != '') {
                $v = ' ' . $this->escape($v);
            }

            $this->ar_having[] = $prefix . $k . $v;
            $this->sqlsrv_having[] = $prefix . $k . ' ?';
            if ($this->ar_caching === TRUE) {
                $this->ar_cache_having[] = $prefix . $k . $v;
                $this->ar_cache_exists[] = 'having';
            }
        }

        return $this;
    }

    // --------------------------------------------------------------------

    /**
     * Insert
     *
     * Compiles an insert string and runs the query
     *
     * @param	string	the table to insert data into
     * @param	array	an associative array of insert values
     * @return	object
     */
    function insert($table = '', $set = NULL) {
        if (!is_null($set)) {
            $this->set($set);
        }

        if (count($this->ar_set) == 0) {
            if ($this->db_debug) {
                return $this->display_error('db_must_use_set');
            }
            return FALSE;
        }

        if ($table == '') {
            if (!isset($this->ar_from[0])) {
                if ($this->db_debug) {
                    return $this->display_error('db_must_set_table');
                }
                return FALSE;
            }

            $table = $this->ar_from[0];
        }

        $this->sqlsrv_param = array_values((array) $set);
        $this->sqlsrv_sql = $this->_insert($this->_protect_identifiers($table, TRUE, NULL, FALSE), array_keys($this->ar_set), array_fill_keys(array_values((array) $set), '?'));
        $sql = $this->_insert($this->_protect_identifiers($table, TRUE, NULL, FALSE), array_keys($this->ar_set), array_values($this->ar_set));

        $this->_reset_write();
        return $this->query($sql);
    }

    /**
     * Insert_Batch
     *
     * Compiles batch insert strings and runs the queries
     *
     * @param	string	the table to retrieve the results from
     * @param	array	an associative array of insert values
     * @return	object
     */
    public function insert_batch($table = '', $set = NULL) {
        $dummy = array();
        if (!is_null($set)) {
            $this->set_insert_batch($set);
            $dummy = $this->set_insert_batch_dummy($set);
        }

        if (count($this->ar_set) == 0) {
            if ($this->db_debug) {
                //No valid data array.  Folds in cases where keys and values did not match up
                return $this->display_error('db_must_use_set');
            }
            return FALSE;
        }

        if ($table == '') {
            if (!isset($this->ar_from[0])) {
                if ($this->db_debug) {
                    return $this->display_error('db_must_set_table');
                }
                return FALSE;
            }

            $table = $this->ar_from[0];
        }

        // Batch this baby
        for ($i = 0, $total = count($this->ar_set); $i < $total; $i = $i + 1000) {
            $sql = $this->_insert_batch($this->_protect_identifiers($table, TRUE, NULL, FALSE), $this->ar_keys, array_slice($this->ar_set, $i, 1000));
            $this->sqlsrv_sql = $this->_insert_batch($this->_protect_identifiers($table, TRUE, NULL, FALSE), $this->ar_keys, array_slice($dummy, $i, 1000));
            $this->query($sql);
        }

        $this->_reset_write();
        return TRUE;
    }

    /**
     * The "set_insert_batch_dummy" function.  Allows key/value pairs to be set for batch inserts
     *
     * @param	mixed
     * @param	string
     * @param	boolean
     * @return	object
     */
    public function set_insert_batch_dummy($key, $value = '', $escape = TRUE) {
        $result = array();
        $key = $this->_object_to_array_batch($key);

        if (!is_array($key)) {
            $key = array($key => $value);
        }

        $keys = array_keys(current($key));
        sort($keys);

        foreach ($key as $row) {
            if (count(array_diff($keys, array_keys($row))) > 0 OR count(array_diff(array_keys($row), $keys)) > 0) {
                // batch function above returns an error on an empty array
                return $result;
            }

            $result[] = '(' . implode(',', array_fill_keys($row, '?')) . ')';
            foreach ($row as $param) {
                $this->sqlsrv_param[] = $param;
            }
        }
        return $result;
    }

    // --------------------------------------------------------------------

    /**
     * Update
     *
     * Compiles an update string and runs the query
     *
     * @param	string	the table to retrieve the results from
     * @param	array	an associative array of update values
     * @param	mixed	the where clause
     * @return	object
     */
    public function update($table = '', $set = NULL, $where = NULL, $limit = NULL) {
        // Combine any cached components with the current statements
        $this->_merge_cache();

        if (!is_null($set)) {
            $this->set($set);
        }

        if (count($this->ar_set) == 0) {
            if ($this->db_debug) {
                return $this->display_error('db_must_use_set');
            }
            return FALSE;
        }

        if ($table == '') {
            if (!isset($this->ar_from[0])) {
                if ($this->db_debug) {
                    return $this->display_error('db_must_set_table');
                }
                return FALSE;
            }

            $table = $this->ar_from[0];
        }

        if ($where != NULL) {
            $this->where($where);
        }

        if ($limit != NULL) {
            $this->limit($limit);
        }

        $this->sqlsrv_param = array_merge(array_values((array) $set), $this->sqlsrv_where_param);
        foreach ($this->ar_set as $k => $v) {
            $sqlsrv_temp[$k] = '?';
        }

        $this->sqlsrv_sql = $this->_update($this->_protect_identifiers($table, TRUE, NULL, FALSE), $sqlsrv_temp, $this->sqlsrv_where, $this->ar_orderby, $this->ar_limit);
        $sql = $this->_update($this->_protect_identifiers($table, TRUE, NULL, FALSE), $this->ar_set, $this->ar_where, $this->ar_orderby, $this->ar_limit);

        $this->_reset_write();
        return $this->query($sql);
    }

    // --------------------------------------------------------------------

    /**
     * Delete
     *
     * Compiles a delete string and runs the query
     *
     * @param	mixed	the table(s) to delete from. String or array
     * @param	mixed	the where clause
     * @param	mixed	the limit clause
     * @param	boolean
     * @return	object
     */
    public function delete($table = '', $where = '', $limit = NULL, $reset_data = TRUE) {
        // Combine any cached components with the current statements
        $this->_merge_cache();

        if ($table == '') {
            if (!isset($this->ar_from[0])) {
                if ($this->db_debug) {
                    return $this->display_error('db_must_set_table');
                }
                return FALSE;
            }

            $table = $this->ar_from[0];
        } elseif (is_array($table)) {
            foreach ($table as $single_table) {
                $this->delete($single_table, $where, $limit, FALSE);
            }

            $this->_reset_write();
            return;
        } else {
            $table = $this->_protect_identifiers($table, TRUE, NULL, FALSE);
        }

        if ($where != '') {
            $this->where($where);
        }

        if ($limit != NULL) {
            $this->limit($limit);
        }

        if (count($this->ar_where) == 0 && count($this->ar_wherein) == 0 && count($this->ar_like) == 0) {
            if ($this->db_debug) {
                return $this->display_error('db_del_must_use_where');
            }

            return FALSE;
        }

        $this->sqlsrv_param = $this->sqlsrv_where_param;
        $this->sqlsrv_sql = $this->_delete($table, $this->sqlsrv_where, $this->ar_like, $this->ar_limit);
        $sql = $this->_delete($table, $this->ar_where, $this->ar_like, $this->ar_limit);

        if ($reset_data) {
            $this->_reset_write();
        }

        return $this->query($sql);
    }

    // --------------------------------------------------------------------

    /**
     * Compile Bindings
     *
     * @access	public
     * @param	string	the sql statement
     * @param	array	an array of bind data
     * @return	string
     */
    function compile_binds($sql, $binds) {
        $this->sqlsrv_sql = $sql;
        $this->sqlsrv_param = $binds;

        if (strpos($sql, $this->bind_marker) === FALSE) {
            return $sql;
        }

        if (!is_array($binds)) {
            $binds = array($binds);
        }

        // Get the sql segments around the bind markers
        $segments = explode($this->bind_marker, $sql);

        // The count of bind should be 1 less then the count of segments
        // If there are more bind arguments trim it down
        if (count($binds) >= count($segments)) {
            $binds = array_slice($binds, 0, count($segments) - 1);
        }

        // Construct the binded query
        $result = $segments[0];
        $i = 0;
        foreach ($binds as $bind) {
            $result .= $this->escape($bind);
            $result .= $segments[++$i];
        }

        return $result;
    }

}

/* End of file sqlsrv_driver.php */
/* Location: ./system/database/drivers/sqlsrv/sqlsrv_driver.php */